﻿namespace SpikingAnalyticsLib.ResponseFingerPrint

//=================================================================================================

/// A response frame
// Wraps a map of firing time offsets for each neuron
type ResponseFingerPrintFrame(offsetMap:Map<int, int list>) =

    // For each window return a list of the spike times inside the window (may be empty)
    let GetWindowSpikeTimesInFrame (combinedMap:Map<(string * int), (int * int * float)>) (frameMap:Map<int, _>) =
        combinedMap
        |> Map.map (fun (patternName, neuronIndex) (minTime, maxTime, _) ->
                if frameMap.ContainsKey neuronIndex then
                    frameMap.[neuronIndex]
                    |> Seq.filter (fun time -> time >= minTime && time <= maxTime)
                    |> Seq.toList
                else
                    []
            )

    // Create a role call of the window spikes in this frame (may be empty)
    // For each window (patternName, neuronIndex), record the presence/absence of spiking in the window
    // A window spike event is the presence or absence of one or more spikes in a window
    // and is defined by a ((patternName, neuronIndex), spiked) triplet
    let GetWindowSpikesInFrame (combinedMap:Map<(string * int), (int * int * float)>) (frameMap:Map<int, _>) =
        combinedMap
        |> Map.toSeq
        |> Seq.map (fun ((patternName, neuronIndex), (minTime, maxTime, _)) ->
                let spiked =
                    if frameMap.ContainsKey neuronIndex then
                        let times = frameMap.[neuronIndex]
                        let windowSpikes =
                            times
                            |> Seq.filter (fun time -> time >= minTime && time <= maxTime)
                            |> Seq.length
                        windowSpikes > 0
                    else
                        false
                (patternName, neuronIndex), spiked
            )
        |> Seq.toList

    // Get the firing events that occur in selected windows in this frame (may be empty)
    // Note that each neuron may have multiple windows from different patterns
    let GetWindowFiringEventsInFrame (combinedMap:Map<(string * int), (int * int * float)>) (frameMap:Map<int, _>) =
        let allWindows =
            combinedMap
            |> Map.toSeq
            |> Seq.map (fun ((patternName, neuronIndex), _) -> neuronIndex, patternName)
            |> Seq.fold (fun (state:Map<int, _>) (neuronIndex, patternName) ->
                    let patternNames =
                        if state.ContainsKey neuronIndex then
                            state.[neuronIndex]
                        else
                            []
                    Map.add neuronIndex (patternName :: patternNames) state
                ) Map.empty

        frameMap
        |> Map.toSeq
        |> Seq.map (fun (nindex, times) ->
                allWindows.[nindex]
                |> Seq.map (fun patternName ->
                        let minTime, maxTime, _ = combinedMap.[patternName, nindex]
                        times
                        |> Seq.filter (fun time -> time >= minTime && time <= maxTime)
                        |> Seq.map (fun time -> time, nindex)
                    )
                |> Seq.concat
            )
        |> Seq.concat
        |> Seq.toList

//---------------------------------------------------------------------------------------------------------
// Properties

    member this.OffsetMap = offsetMap

//---------------------------------------------------------------------------------------------------------
// Methods

    /// Create a list of windows for all patterns and whether they spiked in the current frame
    member this.GetWindowSpikes(combinedMap) =
        GetWindowSpikesInFrame combinedMap offsetMap

    /// Create a list of the firing events that occurred inside windows in the current frame
    member this.GetWindowFiringEvents(combinedMap) =
        GetWindowFiringEventsInFrame combinedMap offsetMap

    /// Collect the spike times that occurred in each window in the current frame
    member this.GetWindowSpikeTimes(combinedMap) =
        GetWindowSpikeTimesInFrame combinedMap offsetMap

    member this.GetScore(patternName, windowMapSet, patternFrequencyMap:Map<string, float>) =
        let frameScore = new FrameScore(this, patternName, windowMapSet, patternFrequencyMap)
        frameScore

//=================================================================================================

/// The score for a response frame and pattern
and FrameScore internal(frame:ResponseFingerPrintFrame, patternName, patternScore:float, windowSpikeActivity:((string * int) * bool) list) =

    // Check that all windows in the list of coincident window spikes exist in the map of combined windows
    static let CheckWindows (combinedWindows:Map<(string * int), _>) (coincidentWindowSpikes:seq<(string * int) * bool>) =
        coincidentWindowSpikes
        |> Seq.forall (fun (window, spiked) -> combinedWindows.ContainsKey window)

    static let CreateScore patternName patternFrequency (combinedWindows:Map<(string * int), (int * int * float)>) (coincidentWindowSpikes:seq<(string * int) * bool>) =
        // compute the product over pActivationGivenPattern for coincident window spikes
        let conditionalProbabilities =
            coincidentWindowSpikes
            |> Seq.map (fun (window, spiked) ->
                    let pXNotXGivenPattern =    // X = Activation
                        let probability =
                            let _, _, windowSpikeCountRatio = combinedWindows.[window]
                            // the empirical spike count ratio can be zero for windows that don't belong to this pattern
                            // therefore ensure that the probability is non-zero 
                            // (see http://en.wikipedia.org/wiki/Naive_Bayes_classifier#Sample_correction)
                            if windowSpikeCountRatio = 0.0 then
                                // some fraction (0.1) of the prior probability (window size/input period w/ 1 Hz background)
                                0.1 * (8.0/250.0)
                            else
                                windowSpikeCountRatio
                        if spiked then
                            probability         // pActivationGivenPattern
                        else
                            1.0 - probability   // pNotActivationGivenPattern MG check
                    System.Math.Log10(pXNotXGivenPattern)
                )
            |> Seq.toList

        // adding the log values (i.e. multiplying)
        System.Math.Log10(patternFrequency) :: conditionalProbabilities
        |> Seq.sum

    let GetScoreSummary testPatternIndex presentedPattern =

        let matched =
            // Match definition:
            // if the presented pattern was the test pattern and the score for the pattern was > 0.9
            // or if presented pattern was NOT the test pattern and the score for the pattern was very low
            if presentedPattern = testPatternIndex then
                patternScore > 0.9
            else
                patternScore < 0.1

        let coincidentSpikes =
            windowSpikeActivity
            |> Seq.filter (fun ((pattern, _), spiked) -> (pattern = testPatternIndex) && spiked)
            |> Seq.length

        matched, patternScore, coincidentSpikes

//---------------------------------------------------------------------------------------------------------
// Constructors

    /// Create a frame score for the specified frame and pattern
    new (frame:ResponseFingerPrintFrame, patternName, (windowMapSet:WindowMapSet), patternFrequencyMap:Map<string, float>) =
        let combinedWindowMap = windowMapSet.GetWindowStatsForPattern(patternName)
        let windowSpikeActivity = frame.GetWindowSpikes(combinedWindowMap)
        let patternFrequency = patternFrequencyMap.[patternName]
        let score = CreateScore patternName patternFrequency combinedWindowMap windowSpikeActivity
        new FrameScore(frame, patternName, score, windowSpikeActivity)

//---------------------------------------------------------------------------------------------------------
// Properties

    /// Get the response frame for this score
    member this.ResponseFrame = frame

    /// Get the pattern name for this score
    member this.PatternName = patternName

    /// Get the score for the current frame
    member this.Score = patternScore

    /// For each window return the spike activity
    member this.WindowSpikeActivity = windowSpikeActivity

    /// Get the number of combined windows with coincident spikes
    member this.CombinedWindowsCoincidentSpikes
        with get() =
            windowSpikeActivity
            |> Seq.filter (fun (_, spiked) -> spiked)
            |> Seq.length

    /// Get the number of windows with coincident spikes for the frame and pattern that specify this frame score
    member this.CoincidentSpikes
        with get() =
            windowSpikeActivity
            |> Seq.filter (fun ((pattern, _), spiked) -> (pattern = patternName) && spiked)
            |> Seq.length

//---------------------------------------------------------------------------------------------------------
// Methods

    /// Get the number of windows belonging to the selected pattern that have coincident spikes
    member this.GetCoincidentSpikes(selectedPatternName) =
        windowSpikeActivity
        |> Seq.filter (fun ((pattern, _), spiked) -> (pattern = selectedPatternName) && spiked)
        |> Seq.length

    /// Get a score description (passed, score, coincident spikes) for the specified test pattern given the presented pattern
    member this.GetScoreDescription(testPatternIndex, presentedPattern) =
        GetScoreSummary testPatternIndex presentedPattern

//=================================================================================================
